chore: merge integration/all-fixes into main (50-commit rollup)#34
Merged
Conversation
Replaces coverage previously held by sphere-sdk's deleted placeholder
`tests/integration/cli/uxf-transfer.test.ts` (sphere-sdk issue #156).
That file pinned the in-tree `cli/index.ts` source which no longer
exists post-extraction; this is a real subprocess integration test
against the same surface in its new home.
Pins:
1. `sphere payments help send` lists --instant / --conservative /
positionals — the help-grep half of the old test.
2. `sphere payments send` with no args exits non-zero with the
documented usage line.
3. `sphere payments send <addr> 0.001 UCT --instant` from an empty
wallet exercises arg-parse → Sphere.init → payments.send →
insufficient-funds error → non-zero CLI exit. The full path is
covered without needing a funded fixture.
4. `sphere payments send <addr> 0.001 UCT --conservative` likewise,
proving the `transferMode` flag reaches the SDK without tripping
the mutual-exclusion guard.
5. `sphere payments send <addr> 0.001 UCT --instant --conservative`
trips the mutual-exclusion guard fast (pre-init), exits non-zero.
6. Funded transfer (gated on E2E_FUNDED_MNEMONIC) — imports a
pre-funded testnet wallet, sends 0.001 UCT to a fresh recipient,
polls receiver balance for receipt. Opt-in to avoid faucet/drain
burden on every test runner.
Suite runs in ~10s end-to-end against the public testnet (1 funded
test skipped without the env var). Verified locally with the latest
sphere-cli build linked against sphere-sdk @ branch
fix/156-cli-test-coverage.
Adds cli-invoice.integration.test.ts (29 tests, 5s offline + 110s e2e): - 14 offline help-shape pins, one per invoice subcommand (create, list, status, pay, close, cancel, return, receipts, notices, auto-return, transfers, export, import, parse-memo). Asserts documented flags and positionals so a help-text drift is caught immediately. - 10 offline arg-validation pins (8 subcommands × no-id usage exit + parse-memo no-memo + one helper test). These exercise the arg-check paths that run BEFORE getSphere() in legacy-cli.ts dispatch, keeping the suite offline-safe. - 5 e2e lifecycle tests against real testnet: empty-wallet list, create, list-after-create, status (OPEN), close (→ CLOSED). Pins the namespace bridge, accounting module wiring, prefix-resolution, and the OPEN → CLOSED state transition end-to-end. Bug fix surfaced by the new tests: invoice-create and invoice-return were passing the 64-char hex coinId from `resolveCoin().coinId` to AccountingModule, which validates coinId as `/^[A-Za-z0-9]+$/` with length ≤20 — i.e. it expects the human-readable symbol (UCT, USDU, ...), not the hex token-type id that `payments.send` uses. Switched to `resolveCoin().symbol` for both call sites; resolveCoin still validates that the symbol is known. Companion fix in sphere-sdk (commit 6f957af on fix/156-cli-test-coverage) addresses the null-dueDate-treated-as-EXPIRED issue that the same e2e lifecycle test discovered while asserting state === 'OPEN'.
Add cli-nametag.integration.test.ts covering the four nametag CLI
subcommands (register / info / my / sync) that lost binary-level coverage
when the in-tree sphere-sdk CLI was extracted. SDK-layer coverage exists
for the underlying registerNametag + transport binding plumbing; this
file pins the CLI plumbing — namespace bridge, arg parsing, help text —
that sits between the user and the SDK.
Three layers, same shape as cli-invoice.integration.test.ts:
1. Help-shape pins (offline, 4 tests) — `payments help <legacy>` for
nametag / nametag-info / my-nametag / nametag-sync. Pins the usage
line + a small set of must-match regexes.
2. Arg-validation pins (offline, 3 tests) — `sphere nametag`,
`sphere nametag register`, `sphere nametag info` with no name. These
exit non-zero with a "Usage: ..." hint before any wallet load
because legacy-cli.ts checks args[1] before getSphere() in both the
`nametag` (~2592) and `nametag-info` (~2619) cases.
3. End-to-end lifecycle (network, 6 tests) — real testnet wallet,
real Nostr relay, real aggregator. Drives:
a. `nametag my` on fresh wallet → "No nametag registered"
b. `nametag info <ghost>` → "not found"
c. `nametag register <random>` → on-chain mint + Nostr publish
d. `nametag my` → returns the registered name
e. `nametag info <registered>` → returns binding record
f. `nametag sync` → re-publishes the binding
Each run mints a fresh `it_<8hex>` nametag to avoid collisions.
13 tests total, all green. Offline subset runs in <2s; e2e in ~28s
against testnet. Gated by SKIP_INTEGRATION=1 like the sibling suites.
Add cli-l1.integration.test.ts pinning the only L1 (ALPHA blockchain)
command exposed by this CLI: `payments l1-balance`. SDK-layer coverage
for L1 balance / Fulcrum / vesting lives in sphere-sdk
`tests/unit/l1/*.test.ts`. What this file pins is the CLI plumbing:
legacy-CLI dispatch, the human-readable output format that wallet
scripts grep.
Scope note: `l1-send`, `l1-history`, and `l1-receive` are NOT exposed
as CLI verbs (only `l1-balance` is wired through legacy-cli.ts ~2168).
The full L1 API is available via `sphere.payments.l1` at the SDK
layer. Memory snapshot for #156 previously listed l1-send as a gap;
correcting that: no L1 send CLI exists to test.
Two layers:
1. Help-shape pin (offline) — `payments help l1-balance` returns the
legacy block with the usage line, "ALPHA" symbol mention, and the
"Fulcrum" connection hint (load-bearing for ops / network policy).
2. End-to-end pin (network) — Fresh testnet wallet → run
`payments l1-balance` → assert the 3-line output block:
"L1 (ALPHA) Balance:"
"Confirmed: <number> ALPHA"
"Unconfirmed: <number> ALPHA"
A fresh wallet has zero balance; assertion is purely on line
structure, not on a specific numeric value.
The "L1 module not available" error path in legacy-cli.ts is
deliberately NOT pinned — Sphere.init() creates the L1 module by
default, so that path is unreachable through this CLI's normal init.
Pinning unreachable error paths produces brittle tests.
2 tests, both green: offline <500ms, e2e ~1s against testnet.
…erage
Add cli-faucet.integration.test.ts pinning the testnet-faucet CLI
surface across all three command aliases (topup / top-up / faucet).
No SDK-layer coverage exists for this — the faucet client is
implemented entirely inside the legacy-CLI handler (~line 2942), so
this file is the only layer that pins it.
Three layers:
1. Help-shape pins (offline, 3 tests) — `payments help <alias>` for
each of topup / top-up / faucet. All three HELP_TEXT entries
(~lines 597-636) live independently; pinning all three catches a
refactor that drops one alias's doc without updating dispatch.
2. No-nametag dispatch pins (network, wallet init only, 3 tests) —
Asserts that running each alias on a fresh wallet (no nametag)
exits non-zero with "No nametag registered" stderr BEFORE any
faucet HTTP call. Proves:
a. namespace registration is asymmetric (only `faucet` is
registered top-level via LEGACY_NAMESPACES; topup / top-up
are reachable via `payments <alias>`)
b. all three names land in the same fall-through case label
c. the precondition fires before the HTTP round-trip so a
broken/rate-limited faucet doesn't mask a wallet-setup error
3. Live faucet round-trip (opt-in, E2E_RUN_FAUCET=1, 1 test) —
Registers a fresh `it_<hex>` nametag, requests 1 UCT from the
faucet, asserts the "Received 1 unicity" success line (pins the
UCT → unicity symbol-to-faucet-name resolution at ~line 2996).
Gated because the faucet has rate limits + drain protection +
external service flakiness; consumes real testnet tokens.
Verified live faucet round-trip succeeds against
faucet.unicity.network — 4 tests run by default + 1 opt-in gated.
Offline ~1s, default e2e ~5s, with-live-faucet ~36s.
Add cli-multiaddress.integration.test.ts pinning the four multi-address
commands (addresses / switch / hide / unhide) plus, critically, the
TOKEN ISOLATION INVARIANT across HD-derived addresses.
The CLI extraction left two distinct things uncovered:
A) CLI plumbing for the multi-address commands (namespace bridge,
arg parsing, help text).
B) The security-critical guarantee that tokens belonging to address
#N never leak to address #M after a switch. A regression here
would mean a user who switched to a fresh address could
accidentally spend tokens that belong to a different HD branch.
The architectural mechanism is per-address token storage: Node.js
FileTokenStorageProvider keeps a separate `tokens/<addressId>/`
subdirectory per tracked address. `sphere.payments.getTokens()` reads
from the storage bound to the currently-active address — so as long
as the directory split is honoured, isolation holds.
Four layers of pins:
1. Help-shape pins (offline, 4 tests) — `payments help <cmd>` for
each of addresses/switch/hide/unhide. HELP_TEXT keys ~707-735.
2. Arg-validation pins (offline, 4 tests) — switch/hide/unhide with
no <index>, plus `switch abc` (non-numeric guard at ~line 2545).
All exit non-zero with "Usage: ..." or "Invalid index" before
getSphere().
3. Stateful local lifecycle (network-light, 7 tests) — fresh wallet
shows only #0 → switch 1 creates+activates #1 with a DIFFERENT
directAddress (HD-derivation isolation pin #1) → on-disk
tokens/ has exactly 2 distinct DIRECT_<...> subdirs (storage
isolation pin #2) → hide/unhide round-trip → switch back to #0
restores the original directAddress exactly (state-preservation
pin #3, no cross-pollination of identity material).
4. Token isolation invariant (opt-in, E2E_RUN_FAUCET=1, 3 tests) —
the gold-standard funded leak proof:
a. faucet 1 UCT at #0; beforeAll polls until UCT lands locally
b. payments tokens at #0 → UCT visible
c. switch to #1 → payments tokens shows "No tokens found"
(THE LEAK TEST — would flip red on cross-address visibility)
d. switch back to #0 → UCT still there, untouched
Gated because on-chain nametag mint (~20s) plus faucet (~5s)
plus the polling loop add ~60-90s on top of the default suite.
Verified with E2E_RUN_FAUCET=1: all 18 tests green in ~111s. The
funded leak proof confirms the isolation invariant holds in practice,
not just in the directory-layout pin.
Implementation note: faucet delivery is async — the faucet API
returns success when the gift-wrap is queued on the relay, not when
the wallet has finalized it into local storage. beforeAll polls
`payments tokens` (with sync) up to 3 times until UCT appears at #0,
then tests use --no-sync for fast per-address reads.
Add cli-wallet-profile.integration.test.ts pinning the five wallet
profile-management subcommands (list / use / create / current / delete)
plus the CROSS-PROFILE ISOLATION INVARIANT.
Note on scope: this fills the "token export/import" gap from the #156
follow-up plan. The CLI has no token-level export/import command — the
closest analogues are `parse-wallet` / `wallet-info` which exist in
legacy-cli.ts but are unreachable through the new namespace dispatch
(dead code). What the CLI DOES expose is profile-level wallet
management, and that surface had zero e2e coverage post-extraction
(cli-wallet.integration.test.ts only covers `wallet init`).
The isolation concern is stronger than the HD-address case pinned in
cli-multiaddress: profiles hold INDEPENDENT MNEMONICS. A leak between
profiles could mean signing transactions with the wrong key or losing
access to a profile entirely. Architectural mechanism: `wallet create
<name>` writes a profile with `dataDir = ./.sphere-cli-<name>` and
flips config.json's active dataDir pointer. `getSphere()` reads from
the current pointer.
Four layers of pins:
1. Help-shape pins (offline, 6 tests) — `payments help "wallet"`,
`"wallet list"`, `"wallet use"`, `"wallet create"`, `"wallet
current"`, `"wallet delete"`. Multi-word HELP_TEXT keys passed as
a single argv element (commander preserves them).
2. Arg-validation pins (offline, 5 tests) — `wallet use/create/delete`
without `<name>` (handlers check profileName before disk write).
`wallet create '!invalid'` rejects bad charset (~line 1849 guard
prevents path-traversal-like names). `wallet bogus-sub` exits 1
via the default `Unknown wallet subcommand` block.
3. CRUD lifecycle (offline, 11 tests) — empty store → create alice →
duplicate-create rejects → current shows alice → create bob
auto-switches → list shows both with → marker on bob → use alice
→ use unknown rejects → delete alice (current) refused → delete
bob succeeds → list no longer shows bob → delete unknown rejects.
4. Cross-profile isolation (network, 3 tests) — init in profile alice
captures directAddrAlice; init in profile bob captures
directAddrBob; expect ≠ alice; both per-profile wallet.json files
exist as separate paths; switch back to alice → `sphere status`
shows alice's directAddress EXACTLY (not bob's).
All 25 tests green in ~11s total. The isolation suite added ~4s on
top of the 7s offline tier — much faster than expected because fresh
profiles don't carry sync state.
Implementation note: `sphere status` prints human-readable output
("Direct Addr: DIRECT://..."), not JSON. The isolation pin matches
that format directly; `wallet init` emits JSON which is matched by
its own regex in the per-profile init steps.
…/verify-balance)
Add cli-wallet-state.integration.test.ts covering four wallet-state
inspection / validation commands that lost binary-level coverage when
the in-tree sphere-sdk CLI was extracted:
- `payments history` — local transaction history
- `payments sync` — pull remote storage into local state
- `payments receive` — finalize incoming gift-wraps
- `payments verify-balance` — validate tokens against aggregator
(spent-token detection)
SDK-layer coverage for each underlying operation exists in
sphere-sdk's PaymentsModule + TokenValidator tests. What this file
pins is the CLI plumbing — exit codes, output shape — that
wallet-management scripts rely on.
Two layers:
1. Help-shape pins (offline, 4 tests) — `payments help <name>` for
each command, asserting documented flags + positionals.
2. Fresh-wallet lifecycle (network, 4 tests) — brand-new testnet
wallet → each command exits 0 with the expected empty-state output:
- history → "Transaction History (last 10):" + "No transactions found"
- sync → exit 0 (no specific output, load-bearing exit code)
- receive → exit 0 (no in-flight gift-wraps)
- verify-balance → "Valid tokens: 0" + "Spent tokens: 0"
Catches the "empty wallet" regression class where 0-token paths
inadvertently rely on a non-empty precondition.
Implementation gotcha pinned: `verify-balance` is NOT a top-level
command — same asymmetric registration as `topup`/`top-up` (only
`faucet` is bare top-level). Reachable only via `payments
verify-balance`. Test uses the working form explicitly.
8 tests, all green: offline ~1s, full e2e ~60s. Per-address /
per-profile isolation is already pinned comprehensively by
cli-multiaddress.integration.test.ts and
cli-wallet-profile.integration.test.ts — this file deliberately
focuses on the command surfaces themselves, not re-running isolation
proofs.
Add cli-assets.integration.test.ts covering the two CLI commands that
surface the global TokenRegistry: `payments assets` (list) and
`payments asset-info` (per-asset details).
SDK-layer coverage for TokenRegistry caching / auto-refresh / race-
safe load lives in sphere-sdk's tests/unit/registry/TokenRegistry.test.ts.
What this file pins is the CLI layer: dispatch, multi-strategy lookup,
output shape.
Three layers:
1. Help-shape pins (offline, 2 tests) — `payments help assets`
(asserts `--type` filter + fungible/nft keywords) and `payments
help asset-info` (asserts `<symbol|name|coinId>` multi-strategy
positional).
2. Arg-validation pin (offline, 1 test) — `payments asset-info`
without identifier exits 1 with usage hint BEFORE getSphere().
3. Network registry queries (4 tests) — fresh testnet wallet drives:
a. `assets` lists at least UCT (proves remote registry fetch +
column-aligned table header)
b. `assets --type fungible` filters out NFTs (no "non-fungible"
in output)
c. `asset-info UCT` returns Symbol/Kind=fungible/Coin ID hex/Network
— pins the symbol-strategy branch of the lookup
d. `asset-info <bogus>` exits 1 with "Asset not found" — pins
the negative path (all 3 lookup strategies failed)
Implementation gotcha pinned: `assets` and `asset-info` are NOT
top-level commands (asymmetric registration, same as topup /
verify-balance). Reachable only via `payments assets` / `payments
asset-info`. Test uses the working form explicitly.
7 tests, all green: offline ~1s, full e2e ~5s.
…nic round-trip
Add cli-wallet-lifecycle.integration.test.ts filling the remaining
wallet-management gaps that cli-wallet.integration.test.ts (only
covers `wallet init` + `wallet status`) and cli-wallet-profile
(only covers profile CRUD list/use/create/current/delete) leave
uncovered:
- `clear` — destructive wipe + --yes confirmation-guard bypass
- `config` — show / set network / dataDir / tokensDir
- `init --mnemonic` — explicit deterministic import round-trip
The most security-critical pin is `clear`'s confirmation guard.
Without it, a user could accidentally wipe their wallet keys (no
recovery without the backed-up mnemonic). The guard demands literal
"yes" stdin input; --yes / -y bypass for scripted contexts. Both
paths pinned:
- `clear` with stdin "no\n" → "Aborted." + wallet survives
(re-verified via `status` still reporting the original
directAddress)
- `clear --yes` → wipe succeeds + `status` reports "No wallet
found"
The BIP-39 determinism round-trip is the strongest integration-level
proof of wallet-recovery correctness:
1. wallet init (with SPHERE_ALLOW_MNEMONIC_NON_TTY=1) → captures
mnemonic + directAddress_A from stdout
2. clear --yes → wallet wiped
3. wallet init --mnemonic <captured> → directAddress_B
4. EXPECT directAddress_A === directAddress_B
Any regression in BIP-39 → seed → HD-derivation → secp256k1 →
bech32 along the entire wallet-recovery pipeline flips this red.
SDK-level coverage exists for each individual step; this is the
only end-to-end CLI pin.
Three layers:
1. Help-shape (offline, 4 tests) — init/status/clear/config blocks.
2. Config get/set (local, no network, 3 tests) — `config` shows JSON,
`config set network dev` mutates + persists, `config set bogus
value` rejects with "Unknown config key" + valid-key hint.
3. Init / clear / re-init round-trip (network, 3 tests) — described
above. Wallet init emits mnemonic to stdout when
SPHERE_ALLOW_MNEMONIC_NON_TTY=1 (test-harness opt-in documented
at legacy-cli.ts ~line 1675).
10 tests, all green: offline ~2s, full e2e ~5.6s.
Finding: Sphere generates 12-word BIP-39 mnemonics, not 24 as some
older docs/comments suggest. The mnemonic regex accepts BIP-39's full
valid range (12, 15, 18, 21, 24 words) anchored to a stdout line.
10 new test files, ~120 tests, ~2500 lines. Pins HD-address isolation, cross-profile isolation, clear confirmation guard, BIP-39 determinism. Fixes invoice-create/return coinId bug.
Pins the binary-level CLI plumbing between users and SwapModule for all
8 swap-* commands: namespace bridge, arg parsing, help-text shape, and
pre-getSphere() validation paths.
- Help-shape pins for 7 commands (Usage line + per-flag regex)
- swap-ping HELP_TEXT gap pin (no help entry; locks current behaviour)
- Arg-validation pins: 6 swap-* commands that require args[1] before
wallet load; refactor moving the check below getSphere() flips red
- swap-propose multi-flag guard: missing flags, partial flags,
out-of-range --timeout (60-86400 sec)
18 tests, all offline, ~4.7s total. No infrastructure dependencies.
Live swap lifecycle (Docker escrow + funded wallets) ships in a
follow-up commit on this branch.
Refs #156
… + faucet) Adds end-to-end coverage of the swap CLI surface against a real escrow. Pairs with the offline tier in 744afa7 to give complete pin coverage of the sphere-cli ↔ SwapModule ↔ escrow protocol path. What lands: - test/integration/local-infra/docker-compose.yml — local NIP-29 relay (port 7778; reserved for group/market follow-up — swap uses public testnet relay) - test/integration/local-infra/relay.ts — relay lifecycle helper, ported from /home/vrogojin/trader-service/test/e2e-live/local-infra - test/integration/local-infra/escrow.ts — escrow container spawn + log-poll for `sphere_initialized` direct address. Image defaults to `escrow:local-uxf` (must be locally built against integration/all-fixes — see docstring). Override with SPHERE_CLI_ESCROW_IMAGE. - test/integration/helpers.ts — `createSphereEnv` now accepts `{ extraEnv }` for callers that need to inject env vars (e.g. SPHERE_NOSTR_RELAYS). - test/integration/cli-swap-e2e.integration.test.ts — gated by E2E_RUN_SWAP=1. Two scenarios on the default tier: 1. swap-ping → "Escrow is online" 2. swap-propose → swap-list on counterparty → swap-cancel → confirm absent from default open list Plus a stretch full-settlement scenario behind E2E_RUN_SWAP_FULL=1 (known fragility: deposit-conclude often stalls because bob's --deposit --no-wait submission may not complete before alice's 300s status budget; tracking as follow-up). Why public testnet relay instead of local: adding a local relay to the wallet's SPHERE_NOSTR_RELAYS list (alongside testnet) caused the faucet gift-wrap to never land in the wallet's inbox. Root cause unclear — under investigation. The simpler architecture (everyone on testnet relay) gives a working 2.5-min default e2e tier. Why the local image: ghcr.io/vrogojin/agentic-hosting/escrow:v0.1 (2026-04-25) predates UXF protocol (PR #105), swap race fixes (#115), verifyPayout OVER_COVERAGE (#119), getTokenIdsForInvoice exposure (#128). Building against integration/all-fixes was required. Tests: - Default tier: 2 scenarios pass (~2.5 min) - Skipped without E2E_RUN_SWAP=1 (default CI fast tier) Refs #156
Expands the 49-line cli-crypto.integration.test.ts to a 37-test
table-driven suite covering all 12 crypto/util commands with
HELP_TEXT entries:
generate-key, validate-key, hex-to-wif, derive-pubkey,
derive-address, base58-encode, base58-decode,
to-smallest, to-human, format, encrypt, decrypt
Three layers, all offline (~10s total):
1. Help-shape pins for all 12 (Usage line + per-flag regexes).
2. Arg-validation pins: 11 commands pre-validate args[1] before
getSphere(). Bare invocation → usage hint + non-zero exit.
3. Behaviour pins:
- generate-key emits pubkey + alpha1 address; secrets hidden
- --unsafe-print SECURITY guard: refuses non-TTY (prevents
leaking the freshly-minted privkey into vitest log buffers)
- validate-key true/false JSON output shape + exit code
- hex-to-wif deterministic WIF for stable test privkey
- derive-pubkey is deterministic (literal pin)
- derive-address is deterministic AND index-sensitive
- base58-encode/decode roundtrip ("Hello" ↔ 9Ajdvzr)
- to-smallest/to-human roundtrip via 8-decimal default
- encrypt → OpenSSL "U2FsdGVkX1" magic header
- decrypt JSON-quoted ciphertext → plaintext
- decrypt wrong-pw does NOT yield original plaintext
(CLI exits 0 — CryptoJS AES-CBC has no HMAC; pin documents
current behaviour and catches pathological keystream collisions)
Refs #156
Adds 2 e2e tests pinning the init-time nametag registration in
cli-wallet-lifecycle:
- `sphere init --nametag it_<hex>` returns identity JSON with the
nametag field populated (proves Sphere.init's nametag option
minted on-chain)
- `sphere nametag my` confirms the binding persisted locally
(proves the registerNametag → storage write path)
The combined flow differs from `init` then `nametag register` in
its failure mode: a mid-mint failure can leave wallet stored but
nametag unregistered. Sphere.init handles this defensively; we pin
the happy path so a regression that drops nametag persistence
during init becomes visible.
Note: `payments validate` was in the follow-up gap list but does
NOT actually exist as a command — only `verify-balance` does (which
cli-wallet-state already pins). No action needed there.
Refs #156
Adds 26 tests pinning the namespace-bridge → dispatcher glue for the
two remaining unaddressed CLI namespaces:
group (9 commands, 17 tests):
create / list / my / join / leave / send / messages / members / info
— help-shape + arg-validation pins for the NIP-29 group chat surface.
market (5 commands, 9 tests):
post / search / my / close / feed
— help-shape + arg-validation pins for the P2P bulletin-board surface.
Including market-post's two-step pre-getSphere() guard (description
positional, then --type required flag).
Live e2e tiers deliberately deferred:
- group: would need a NIP-29-capable local relay (the unicity-tokens-
relay used by the swap suite is generic nostr-rs-relay; not
confirmed to handle NIP-29 moderation events). SDK-level coverage
exists in sphere-sdk tests/relay/groupchat-relay.test.ts.
- market: would need long-form NIP-23 relay + broadcast network.
SDK-level coverage covers the module mechanics.
The offline tier here is enough to catch the failure modes that hurt
users most: a refactor renames a flag (silent break), or moves the
arg check below getSphere() (turning "did I type the command right?"
into a 10-second wallet load). Live roundtrips are SDK-team territory.
Refs #156
Addresses code-reviewer feedback before merging into integration/all-fixes.
BLOCKER fix:
- escrow.ts:175 — UNICITY_RELAYS → UNICITY_NOSTR_RELAYS. The escrow
service's acp-adapter reads UNICITY_NOSTR_RELAYS (with
SPHERE_NOSTR_RELAYS fallback). The old var name was silently
ignored, so the container fell back to network defaults regardless
of opts.relayUrl. Worked today only because the e2e suite targets
the public testnet relay (which is also the network default);
pointing at a local Nostr relay would have failed silently.
IMPORTANT fixes:
- escrow.ts:168 — UNICITY_MANAGER_DIRECT_ADDRESS now uses
`DIRECT://<pubkey>` form, not raw pubkey hex. The current escrow
code only checks for non-empty, but a future routing change would
dereference it as a transport address; the placeholder needs to
be syntactically correct.
- helpers.ts — MaxListenersExceededWarning from accumulating exit
handlers (3 per re-evaluation × 14 test files = 42 vs default
limit of 10). Guard against re-registration via a global symbol
so the three process.once handlers fire at most once per worker
regardless of how many test files import this module.
NIT fixes:
- escrow.ts materializeWalletDir — 0700 chmod on the wallet dir
+ mkdir mode hardening. Matches the helpers.ts pattern. Paranoia
for non-POSIX platforms where mkdtemp may inherit DACLs.
- cli-swap-e2e.integration.test.ts — added inline comment on the
full-settlement describe.skipIf block documenting why the tier is
gated, the known failure mode, and likely fix paths.
- cli-crypto.integration.test.ts — replaced "known shortcoming"
wording with explicit TODO(security) marker that flags the
decrypt-wrong-pw-exits-0 path as a CLI defect to be addressed in
a separate PR (move to AES-GCM with backward-compat shim).
Test plan:
- Typecheck: clean
- Offline tier: 162 passing / 55 skipped / 0 failed (44s)
- E2E_RUN_SWAP=1: ping + propose/list/cancel pass (~2.2 min)
- Sigint/sigterm cleanup for the escrow Docker container is NOT
addressed in this commit (reviewer's lowest-severity nit; the
container is visible via `docker ps` and easy to clean up
manually).
Refs #156
…group + market Fills the remaining CLI test gaps from PR #13's follow-up comment on issue #156. 6 commits, +1,827 lines, +89 tests. Coverage delta: - swap — 18 offline + 3 e2e (2 default + 1 stretch-gated) - crypto/util — 37 offline (expanded from 3) - init --nametag — 2 e2e - group — 17 offline - market — 9 offline Infrastructure additions: - test/integration/local-infra/ (docker-compose.yml, relay.ts, escrow.ts) for swap e2e + reserved for group/market e2e - helpers.ts: createSphereEnv accepts { extraEnv }; MaxListeners guard fixes 11-listener warning Review fixes (PR #14): - escrow.ts UNICITY_RELAYS → UNICITY_NOSTR_RELAYS (silent ignore bug) - UNICITY_MANAGER_DIRECT_ADDRESS now uses DIRECT:// prefix - Wallet dir 0700 hardening matches helpers.ts pattern - Inline docs on swap full-settlement fragility - TODO(security) marker on decrypt-wrong-pw bug Critical caveat for downstream: ghcr.io/vrogojin/agentic-hosting/escrow:v0.1 is stale vs integration/all-fixes; the swap e2e tier requires a locally built escrow:local-uxf image (build steps in escrow.ts docstring). A future agentic-hosting v0.2 publish would remove this requirement. Test status: - Offline tier: 162/162 passing - swap e2e default: 2/2 passing (~2.2 min) - swap e2e stretch: gated, known fragile
Published 2026-05-16: ghcr.io/vrogojin/agentic-hosting/escrow:v0.2 digest sha256:311903b6f98b33a63791bf79db6522a66d118588ba56fcf6e56654ed6670ebac Composition: escrow-service @ d427e5d (master + fix/conservative- payout-mode HEAD) + uxf sphere-sdk @ 3a575cd (integration/all-fixes HEAD). Removes the requirement that every developer / CI runner locally build escrow:local-uxf before running the swap e2e suite. Verified: E2E_RUN_SWAP=1 npm run test:integration -- cli-swap-e2e passes against the published image (2/2 default tier, ~2.3 min). SPHERE_CLI_ESCROW_IMAGE env var still lets you override with a locally-built dev image (build steps in the escrow.ts docstring). Follow-up: formalize the publish path so future v0.3 goes via the escrow-service GitHub release workflow (currently pins SPHERE_SDK_SHA to a Apr 9 commit; needs bump to 3a575cd). Tracked separately. Refs #156
Replaces escrow:local-uxf with ghcr.io/vrogojin/agentic-hosting/escrow:v0.2 as the default. Removes the developer-rebuild requirement for swap e2e. Override via SPHERE_CLI_ESCROW_IMAGE still available. v0.2 composition documented in escrow.ts docstring: escrow-service@d427e5d + uxf sphere-sdk@3a575cd (integration/all-fixes), digest sha256:311903b6f98b33a63791bf79db6522a66d118588ba56fcf6e56654ed6670ebac. Verified: E2E_RUN_SWAP=1 default tier 2/2 passing against the published image (~2.3 min).
#163 item 1. The full-settlement tier (E2E_RUN_SWAP_FULL=1) was
gated behind "known fragile" because bob's
`swap accept --deposit --no-wait` returned after SUBMITTING the
deposit, not after on-chain CONFIRMATION. Alice then ran her own
`swap deposit` synchronously, but by the time we started polling
alice's status for `completed`, bob's deposit was often still
in-flight — escrow can only conclude after seeing both, so settlement
stalled at `depositing`, exhausting the 300s budget.
Restructured:
- bob accepts WITHOUT --deposit (just announces the swap).
- alice + bob both run `swap deposit` IN PARALLEL via the new
`runSphereAsync` helper. Each command BLOCKS until its deposit
is confirmed on-chain (the SDK waits for inclusion proof + escrow
ack), so when Promise.all resolves both deposits are definitely
observable to the escrow.
- Status poll budget bumped 300s → 600s as a defensive safety net.
- Outer test timeout 600s → 900s to accommodate slower testnet days.
Added `runSphereAsync` to helpers.ts — a Promise-based variant of
`runSphere` using `child_process.spawn`. Same `SpawnSyncReturns` shape
so existing assertions work unchanged. Same SIGKILL-on-timeout
semantics as the sync wrapper.
Verified:
- npx tsc --noEmit clean
- npx eslint test/integration/{helpers,cli-swap-e2e.integration.test}.ts clean
- SKIP_INTEGRATION=1 npx vitest run … cli-swap-e2e — file loads, tests
correctly skipped (3 tests / 2 skipped due to gate)
- Live E2E_RUN_SWAP_FULL=1 run pending; documented in PR follow-up.
…s root cause 3 live runs (parallel + sequential variants, 600s budget) all failed with the same pattern: escrow's payments-module profile manifest update fires `[PerTokenMutex] bounded-hold ... manifest CID rewrite CAS failure: cas-mismatch` after both deposits arrive (sequential deposits at ~50s apart). Swap stalls at `PARTIAL_DEPOSIT` → `invoice:covered with unconfirmed deposits — waiting for aggregator confirmation` and never advances. Filed separately as a sphere-sdk issue — unblocking this e2e tier depends on the escrow image picking up that fix (escrow:v0.3+). Kept in this PR (independently useful): - `runSphereAsync` helper for parallel CLI invocations. - Wait-for-announced poll loop (prevents alice's `swap deposit` from racing its own 60s event-wait against escrow's invoice-delivery DM). - Sequential deposit ordering (bob `accept --deposit --no-wait` then alice `swap deposit`). - Budget bumps 300s → 600s + outer 600s → 900s for when the escrow bug lands. Updated inline docstring with the full finding so the next person picking up this test understands the actual blocker. Test remains gated behind `E2E_RUN_SWAP_FULL=1` (it does not pass yet — the escrow CAS-mismatch must be fixed upstream first).
The escrow:v0.3 image (published 2026-05-21) bundles sphere-sdk PR #196
which resolves the issue #195 root cause of the full-settlement hang:
1. Placeholder manifest entry CAS-mismatch in the recipient finalization
worker's poll callback (eliminates the `[PerTokenMutex] bounded-hold
... manifest CID rewrite CAS failure: cas-mismatch` operator-dashboard
noise on every inbound deposit).
2. Missing `transfer:confirmed` emit in the recipient dispositionWriter
VALID branch (AccountingModule now re-fires `invoice:covered` with
`confirmed: true` — the signal the escrow swap orchestrator gates
on to advance past PARTIAL_DEPOSIT).
Verification (2026-05-21, against published `ghcr.io/vrogojin/agentic-hosting/escrow:v0.3`):
Test Files 1 passed (1)
Tests 3 passed (3)
✓ swap ping 1947ms
✓ propose + cancel 36622ms
✓ full deposit settlement (E2E_RUN_SWAP_FULL=1) 131147ms
Duration 266.02s
Full settlement reaches `completed` in 131s — comfortably under the
600s polling budget kept from the prior amendment.
Changes:
- `test/integration/local-infra/escrow.ts`: bump default
`ESCROW_IMAGE` from v0.2 → v0.3. Refreshed the in-source docstring
with the v0.3 composition (PR #196 callout + v0.2-is-now-stale note).
Override mechanism (`SPHERE_CLI_ESCROW_IMAGE`) unchanged.
- `test/integration/cli-swap-e2e.integration.test.ts`: updated the
file-level gate-comment v0.1 reference → v0.3, and rewrote the
full-settlement section's investigation docstring to reflect the
resolved state (the comment block previously documented the
bug-still-open state and the rationale for keeping the polished
test infrastructure as defensive code).
…ttlement test(swap): #163 — fix flaky full-settlement e2e via parallel deposits
`sphere invoice create --target @bob-tag --asset "1000000 UCT"` now resolves the @NameTag (or chain pubkey, or alpha1 address) to the canonical `DIRECT://` address before calling `AccountingModule.create Invoice`. Symmetric with `payments send --recipient @nametag` which already accepts these forms. Why resolve at the CLI layer: - `AccountingModule.createInvoice` validates `target.address.starts With('DIRECT://')` (modules/accounting/AccountingModule.ts:906) and throws `INVOICE_INVALID_ADDRESS` otherwise. This is correct SDK behaviour — invoice terms cryptographically bind the recipient identity, so the canonical DIRECT:// form is what gets signed and shipped. - The CLI is the right layer to translate user-facing identifiers (@NameTag, chain pubkey, alpha1) into canonical addresses. The same pattern is already in `dm-history` (legacy-cli.ts:3108) and in `payments send --recipient`. - Resolution happens once at create-time; the resolved DIRECT:// address is what's persisted in the invoice's signed terms, so a later nametag rename does NOT invalidate the invoice (which is the correct semantic). Before this fix: $ sphere invoice create --target @bob-tag --asset "1000000 UCT" Error: Invalid target address: must be DIRECT:// format After: $ sphere invoice create --target @bob-tag --asset "1000000 UCT" Invoice created: { ... "address": "DIRECT://0000..." ... } Validated against live testnet during issue-223 cross-process manual recovery test (see sphere-sdk's manual-test-full-recovery.sh + walkthrough doc on PR #222 / branch docs/issue-218-full-recovery-manual-test). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…gration/all-fixes tip The previous pinned SHA `86468103a` was the tip of `refactor/extract-cli-to-sphere-cli` at the time the workflow was added. That branch has since been deleted from the public sphere-sdk repo. The commit still exists in the GitHub repo's object database but is no longer reachable via any branch tip, so a default `git clone` does not fetch it and the subsequent `git checkout --detach $SHA` fails with `fatal: unable to read tree`. PR #17's CI started failing for this reason — symptom unrelated to PR contents. Fix: re-pin to `02cb4550fac` (the tip of `integration/all-fixes` after PR #225, the cross-process UXF delivery fix). That branch contains the same CLI-consumed type exports (`CreateInvoiceRequest`, `PayInvoice Params`, `InvoiceRequestedAsset`, encrypt/decrypt helpers, ...) that the original pin provided. Verified locally: grep -E 'CreateInvoiceRequest|PayInvoiceParams|InvoiceRequestedAsset' \ sphere-sdk/index.ts CreateInvoiceRequest, InvoiceRequestedAsset, PayInvoiceParams, Defense-in-depth: add `git fetch origin "$SPHERE_SDK_SHA" || true` before checkout so the workflow keeps working when integration/all- fixes advances past this commit (the SHA stays pinned for supply-chain integrity, but the explicit fetch picks it up via the object database even if it's no longer on a branch tip). Both `ci.yml` and `integration-nightly.yml` updated together so a nightly run stays hermetic with PR CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Surfaces the SDK's new `accounting.deliverInvoice()` API at the CLI.
Packages a previously-minted invoice into a UXF bundle and ships it
to every non-self target via NIP-17 DM, so payers' wallets can
discover the invoice without out-of-band coordination.
Usage:
sphere invoice deliver <id-or-prefix> [--to <recipient>...] [--memo <text>]
Behaviour:
- Prefix-matches the invoiceId against the local ledger (same lookup
pattern as invoice-pay).
- Default recipients: every non-self DIRECT:// target in the invoice
terms (multi-HD self-skip honoured by the SDK).
- --to is repeatable for explicit recipient override (@NameTag,
DIRECT://, chain pubkey — same resolver pool as `payments send`).
- --memo decorates the DM envelope (display-only, not part of the
bundle hash).
Output:
- JSON `DeliverInvoiceResult` with `{ invoiceId, sent, failed,
skippedSelf, recipients[] }` for scripting.
- Exit code 0 on full success, 2 on partial failure (any recipient
failed). Operators can grep `recipients[].error` for the cause.
Companion to integration/all-fixes commit 90a5cc3 on sphere-sdk.
The manual-test-full-recovery.sh §C calls this between
`invoice create` and Bob's `invoice pay` to validate end-to-end.
…iver
legacy-cli.ts wraps `process.exit` to schedule an async teardown
(destroys the Sphere instance, closes Nostr relays, IPFS handles)
before the real exit. The wrapper returns `undefined as never`, so
synchronous control flow continues past `process.exit(N)` until the
async teardown's `.finally(() => originalExit(code))` fires later.
Without an explicit `return` after `process.exit(1)`, the catch in
invoice-deliver's main flow logged the SDK error correctly but then
the next statements ran: `console.log('Invoice delivery result:')`
printed `undefined`, then `result.failed > 0` crashed with
"Cannot read properties of undefined (reading 'failed')" — clobbering
the original SDK error message in the operator's terminal.
Surface caught during manual-test-full-recovery.sh §C.1b live testnet
run (sphere-sdk log /tmp/manual-cli-test-226-v3.log). Mirror fix
applied for the `result.failed > 0` partial-failure exit so the
post-result code is symmetric.
Same pattern needed by every handler in this file that has post-catch
work — kept the comment block explanatory so a future audit catches
the rest.
`sphere daemon start --detach` returned exit 0 with a PID, but the forked child died between fork() and any useful work — leaving a stale PID file and an empty daemon.log. Two compounding causes: 1. The parent forked with `stdio: 'ignore'`, which strips the IPC channel that child_process.fork() normally establishes. The child's `process.connected` was therefore false from the start. 2. The child unconditionally called `process.disconnect()` (guarded only by `if (process.disconnect)`, which is always truthy because the function exists regardless of channel state). With no live IPC channel, disconnect throws "IPC channel is not open". The throw bubbled up to legacy-cli's catch, which called `console.error` (already redirected to an unflushed WriteStream) and `process.exit(1)`. The child died silently — the WriteStream's pending writes never flushed because the underlying fs.open hadn't completed. Fix: * `detachDaemon` now opens the log file in the parent and inherits the fd to the child's stdout AND stderr (`stdio: ['ignore', logFd, logFd, 'ipc']`). Any future startup failure — crash, uncaught exception, raw stderr emission — is captured at the OS level before any Node-level streaming machinery is required. 'ipc' is kept because child_process.fork() throws "Forked processes must have an IPC channel" without it; the channel exists solely to satisfy fork's contract (no messages flow over it). * `runDaemon`'s child-side disconnect is now guarded on `process.connected`, so it correctly no-ops if the channel was already torn down (e.g. parent exited first) and only disconnects when there's a live channel to release. * `log()` no longer double-writes in forked mode (the redirected `console.log` already forwards to the same WriteStream that `log` writes to directly). Pre-existing bug, exposed only because the daemon now actually runs. Reproduction from the issue (`mkdir … && wallet create … && init … && daemon start --detach …`) now reports "Daemon is running (PID X)" after sleep 3, with a non-empty daemon.log containing "Daemon running. Waiting for events." — matching acceptance criteria.
New integration suite per acceptance criteria. Mirrors the pattern of
test/integration/cli-wallet-lifecycle.integration.test.ts (offline
help-shape layer + network lifecycle layer skipped by SKIP_INTEGRATION).
Two layers:
1. Help-shape (offline, 3 pins) — `payments help daemon`,
`daemon start`, `daemon status`. Pins the documented flag surface
(--detach, --event, --action, --log, --pid) so a refactor that
drops a flag from the help registry fails red.
2. Detach lifecycle (network, 1 pin) — end-to-end:
start --detach → exit 0 with "Daemon started in background"
sleep 6s + status → "Daemon is running (PID X)"
(was: "Daemon is not running (stale PID
file, process X)")
log file → non-empty, contains
"Daemon running. Waiting for events."
stop → "Daemon stopped"
post-stop status → "Daemon is not running"
PID file → removed
afterEach calls `daemon stop` defensively so a mid-test failure cannot
leak a forked daemon holding open Nostr WebSocket connections against
the testnet relay.
Why this is an integration test (not unit / mocked): the bug fixed by
issue #19 was inside child_process.fork() + process.disconnect()
semantics that only manifest when an actual node process is forked with
the actual stdio + IPC-channel configuration. A unit test mocking fork
would not catch a recurrence.
Self-review follow-up. The parent's child.disconnect() closes the IPC channel at the OS layer, but the child's JS-level 'disconnect' event (which flips process.connected to false) is delivered async. There's a narrow microtask window where the child reads process.connected as true while the underlying channel is already torn down, in which case the disconnect() call throws "IPC channel is not open" — the exact failure mode this PR fixes, just triggered by a different cause. Wrap the child-side disconnect in try/catch. Swallowing is correct: the goal state (channel closed) already holds either way. In practice the race window is small (child needs ~hundreds of ms to load and reach the disconnect call, by which time the event handler has run) and integration tests have not hit it, but the defensive guard removes a theoretical flake source from production deployments.
…e-exit-19 fix(daemon)(#19): keep --detach child alive past process.disconnect()
… stop
`legacy-cli.ts`'s `main()` wraps `process.exit` to destroy the Sphere
instance (Nostr relays, IPFS handles, SQLite) before the real exit.
Cleanup is async, so the wrapper used to schedule `inst.destroy()
.finally(originalExit)` and `return undefined as never`. That left the
calling line of code to continue executing past `process.exit(N)`.
The shape that surfaced this was `invoice-status`:
if (matched.length === 0) {
console.error('No invoice found matching prefix: ...');
process.exit(1); // wrapper schedules destroy, returns
}
const invoiceId = matched[0].invoiceId; // ← matched[0] undefined
…which crashed with `Cannot read properties of undefined (reading
'invoiceId')`. Every other `invoice-*` handler (close, cancel, pay,
return, receipts, notices, transfers, export) shares the same shape,
and there are ~180 `process.exit(N)` call sites across this file that
all depend on synchronous termination.
The wrapper now throws an `ExitSignal` sentinel synchronously when a
Sphere instance is loaded. The outer try/catch in `main()` detects
`ExitSignal`, awaits `closeSphere()`, and forwards the code through
the original (non-wrapped) `process.exit` so the catch is not re-
entered. When no instance is loaded (early arg-validation paths), the
wrapper falls straight through to `originalExit`, matching the
previous synchronous behaviour for help / usage exits.
ExitSignal deliberately does not extend `Error` so inner
`catch (err)` blocks that filter on `err instanceof Error` do not
classify it as a normal error worth logging. Every inner catch in
this file either re-calls `process.exit(N)` (which re-throws an
ExitSignal that propagates correctly) or sits over a try body with
no `process.exit` (so ExitSignal can never reach it) — audited via
the catch-block sweep in `src/legacy/legacy-cli.ts`.
Regression pins in `test/integration/cli-invoice.integration.test.ts`
(lifecycle block, gated by `integrationSkip`):
- `invoice status <unknown-prefix>` → exit 1 + clean error message,
no `Cannot read properties of undefined` / `TypeError` anywhere.
- Companion `it.each` for `close` / `cancel` / `pay` — same shape,
catches a wrapper regression surfacing on any of these handlers.
Manual repro (matches issue body) on testnet:
sphere wallet create alice
sphere wallet use alice
sphere init --network testnet --nametag inv-crash-xxx
sphere invoice status 00005eb450a21d54f6d77b3c352a26a7539cc453ccdb1d1928dcdb6a0a266ca31e82
→ No invoice found matching prefix: …
→ exit 1, no stack trace ✓
The prior bf40221 fix (`fix(invoice)(#226)`) added an explicit
`return` after the catch-block `process.exit(1)` in invoice-deliver.
That `return` is now unreachable (the throw propagates first) but
left in place as a defensive marker — removing it widens this PR's
scope unnecessarily.
Related: #19 / #20 (daemon detach) surfaced this bug indirectly by
unblocking manual-test-full-recovery.sh §B → §C.4, where peer2-alice
hit `invoice status` for an invoice it had never received. The
cross-device invoice sync gap (peer2 not seeing peer1's invoice) is
the SDK-side follow-up tracked separately; this commit only fixes
the CLI crash that was masking it.
… guard The defensive `return` in invoice-deliver's catch dates from #226 when the `process.exit` wrapper returned `undefined as never` and required explicit returns at every call site. With #21's wrapper rewrite the ExitSignal throw propagates first, so the comment's "wrapper returns undefined" wording is now wrong and would confuse a future reviewer auditing why the `return` is there. Keep the `return` itself — defensive marker against a wrapper regression that reintroduces the fall-through. Rewrite the comment to describe the current ExitSignal-based mechanism and the regression class it guards against. No behaviour change. Pure documentation drift cleanup.
fix(cli)(#21): throw ExitSignal from process.exit wrapper so handlers stop
Replace the deprecated IpfsStorageProvider bootstrap (IPNS-based
last-writer-wins sync) with createNodeProfileProviders (OrbitDB +
aggregator pointer + IPFS CAR). Wraps the multi-device data-loss
window flagged during #223 §D.4 validation.
Phase 1 of the migration plan:
- New shared helper `src/shared/sphere-providers.ts` exposing
`buildSphereProviders()` (merges createNodeProviders' transport/
oracle/etc. with createNodeProfileProviders' storage/tokenStorage)
and `detectWalletKind()` (pure filesystem read — orbitdb/ marker
classifies profile vs legacy wallets).
- `getSphere()` in src/legacy/legacy-cli.ts now boots Profile and
short-circuits with EX_TEMPFAIL (75) + a clear `sphere wallet
migrate` prompt when a legacy on-disk layout is detected. The
`--mnemonic` seeding paths are exempt from the gate so wallet
recovery against an existing dataDir still works.
- `clear` command picks the provider bundle by detected kind so a
legacy-only wallet doesn't have to spin up OrbitDB just to wipe
empty Profile state.
- `src/host/sphere-init.ts` adopts the combined providers and the
same legacy guard — host commands cannot operate against a
pre-migration wallet without misrouting.
- New `sphere wallet migrate [--apply]` subcommand. Default is a
strictly side-effect-free dry-run that uses ONLY the legacy
provider bundle (no Profile boot, no orbitdb/ created), counting
legacy tokens via sphere-sdk's `importLegacyTokens(... dryRun:
true)`. `--apply` boots Profile and runs the non-destructive
import; legacy files stay on disk.
Tests:
- 6 unit tests for detectWalletKind (fresh / legacy / profile /
edge cases).
- 2 dispatch-table tests for `sphere wallet migrate` routing.
- 6 end-to-end integration tests in cli-wallet-migrate.integration
.test.ts driving the full lifecycle against real testnet:
init → simulate legacy by `rm -rf orbitdb/` → gate trips with
exit 75 → dry-run reports inventory without recreating orbitdb/
→ --apply restores Profile path → subsequent commands no longer
trip the gate. ~9s wall-clock on testnet.
Deferred to Phase 2/3 (per the phased PR plan):
- manual-test-full-recovery.sh §D.4 validation.
- Migrate src/pointer/sphere-init.ts to use the shared helper
(still uses its own dynamic-import shim).
- Optional `--archive` flag on `wallet migrate` to move legacy
data into ./.sphere-cli/legacy-backup/.
- Integration test for `importLegacyTokens` actually moving N>0
tokens across the boundary (current e2e covers wiring only —
fabricating valid TxfToken files is its own task).
Self-review caught a dead helper. The migrate command in src/legacy/legacy-cli.ts calls createNodeProviders directly; nothing else imported buildLegacyOnlyProviders. Per CLAUDE.md guidance against unused abstractions, remove it now — a future PR can re-add a focused helper when an actual caller needs one.
…iders feat(cli)(#23): bootstrap Profile providers; prompt on legacy wallets
Both commands need the IPFS / Profile pointer pull, not just the Nostr inbox. 'nostr' mode skipped that pull, so on a fresh device or after a wipe `invoice-status` reported "No invoice found matching prefix" and `invoice-list` returned an empty set — even when the invoice had been minted by another peer and was reachable on-chain. The other invoice commands (deliver, close, cancel, pay, return, receipts, notices, auto-return, transfers, export) already used 'full'. This aligns the two read-side commands with the rest. Companion sphere-sdk fix is #230 — once that lands, the receiver-side AccountingModule.invoiceTermsCache will also refresh on sync:completed and the full cross-device §C.4 flow will work end-to-end. Refs sphere-cli#24, sphere-sdk#230, sphere-sdk#223
…s-sync-mode fix(cli)(#24): invoice-status and invoice-list use 'full' sync mode
…older
The `sphere wallet use <name>` flow constructs a FileStorageProvider
whose `connect()` writes an empty `{}` JSON to wallet.json as a side
effect — BEFORE any wallet data exists. PR #25's `detectWalletKind`
classified that placeholder as `legacy` and tripped the migrate gate
on every fresh wallet, blocking `sphere init` for first-time users.
Caught by `manual-test-full-recovery.sh §1`: peer1-alice `sphere init`
exited 75 immediately with "Legacy wallet detected" instead of
proceeding to mint the nametag.
Fix: parse `wallet.json` and classify an empty top-level object as
`fresh`. A wallet.json with any key is still treated as `legacy`
(real wallet data → migrate triage). An unparseable / non-object
file is also conservatively routed through `legacy` so a corrupted
or unrecognized file isn't silently clobbered by a Profile boot.
Tests (5 new in `src/shared/sphere-providers.test.ts`):
• empty `{}` placeholder → fresh
• `{}` with whitespace → fresh
• single key (`{"mnemonic":"..."}`) → legacy (unchanged)
• unparseable garbage → legacy (conservative)
• array shape `[]` → legacy (unexpected shape)
All 119 unit tests pass. Typecheck clean. No new lint warnings.
Refs sphere-cli#23, PR #25.
…ty-placeholder fix(cli)(#23): detectWalletKind ignores empty `{}` wallet.json placeholder
…lock The daemon parks the event loop forever with OrbitDB / Helia open; LevelDB takes a POSIX advisory file lock (fcntl(F_SETLK)) on <dataDir>/orbitdb/<dbAddress>/_index/LOCK and on <dataDir>/datastore/LOCK. A sibling CLI in the same dataDir hits LEVEL_LOCKED -> 'Database is not open', and the bounded retry from sphere-sdk PR #246 can never succeed (the contention isn't transient). This short-term gate detects the live-daemon case in getSphere() and exits with EX_TEMPFAIL, telling the operator to 'sphere daemon stop' first. Skipped when our own PID owns the PID file (daemon-start calling back into getSphere is the legitimate owner). Bypassed for daemon stop/status (which don't go through getSphere). The proper fix is a daemon-as-broker IPC surface (sphere-sdk #247 long-term: Unix domain socket at <dataDir>/.sphere-cli/daemon.sock, RemoteOrbitDbAdapter mirroring the OrbitDbAdapter interface). Until then, this stops the script-level cascade observed at §C.4 in manual-test-full-recovery.sh. Exports readPidFile and isDaemonProcessAlive from daemon.ts so legacy-cli.ts can reuse them without duplication.
fix(cli)(sphere-sdk#247): refuse CLI when a sphere daemon holds the OrbitDB lock The daemon parks the event loop forever with OrbitDB / Helia open; LevelDB takes a POSIX advisory file lock (fcntl(F_SETLK)) on <dataDir>/orbitdb/<dbAddress>/_index/LOCK and on <dataDir>/datastore/LOCK. A sibling CLI in the same dataDir hits LEVEL_LOCKED -> 'Database is not open', and the bounded retry from sphere-sdk PR #246 can never succeed (the contention isn't transient). Short-term gate in getSphere(): detects the live-daemon case and exits with EX_TEMPFAIL telling the operator to 'sphere daemon stop' first. Skipped when our own PID owns the PID file (daemon start callback into getSphere is the legitimate owner). Bypassed for daemon stop/status (don't go through getSphere). The long-term fix is a daemon-as-broker IPC surface (sphere-sdk #247 follow-up: Unix domain socket + RemoteOrbitDbAdapter).
Residual #2 of the §D `manual-test-full-recovery.sh` ALL GREEN campaign. The `sphere wallet use <name>` subcommand printed its confirmation lines ✓ Switched to wallet profile: <name> Nametag: <tag> L1 Addr: <alpha1...> via `console.log` (stdout). The harness captures `sphere balance > file` snapshots; some snapshot blocks bracket the `wallet use` invocation outside the redirect (peer2: `sphere wallet use alice ; sphere balance > file`), others inside a subshell (peer1: `( … sphere wallet use alice && sphere balance ) > file`). The two flows yield different captured-stdout content for the same logical operation, so the resulting peer1-vs-peer2 diff failed assertion even though both wallets had identical balances. Fix: route the entire confirmation block (success and `(wallet not initialized in this profile)` fallback) through `console.error`. Errors (usage hint, profile-not-found) were already on stderr, so this brings the success path in line with them. Behaviour for human operators is unchanged — terminal sessions still see the banner; only `>` / `|` stdout pipelines are now unaffected by it. Side-benefit: any future shell tooling that pipes `sphere wallet use <name> | …` no longer has to filter the banner out of the consumed stream. Tests * `test/integration/cli-wallet-profile.integration.test.ts` — the "`wallet use alice` switches the active profile" assertion now expects the banner on `r.stderr` (with a negative match on `r.stdout`) per the new contract. * Full integration suite: 25 / 25 passed. * Full default unit suite: 119 / 119 passed. Refs sphere-sdk#282
…tderr fix(cli)(sphere-sdk#282): route `wallet use` confirmation to STDERR
…uildSphereProviders (#31) Closes the CLI half of sphere-sdk issue #394. `buildSphereProviders` now imports `createUxfCarPublisher` + `DEFAULT_IPFS_GATEWAYS` from `@unicitylabs/sphere-sdk/impl/nodejs` (re-exported on the SDK side as part of #394) and exposes: - `publishToIpfs` — outgoing UXF CID-delivery callback wired from `createUxfCarPublisher(ipfsGateways)`. - `cidFetchGateways` — recipient-side fetch list so `uxf-cid` bundles resolve correctly on arrival. Both flow through `SphereProvidersBundle` and into `Sphere.init`: - `src/host/sphere-init.ts` adds explicit pass-through (the call site unpacks named fields, not a spread). - `src/legacy/legacy-cli.ts` is untouched — its `Sphere.init` already spreads `...initProviders`, which inherits the new fields automatically. Same for the migration call site at line ~2240. Crucially, this does NOT re-enable the deprecated `IpfsStorageProvider` (deprecated for wallet token storage; replaced by Profile). The UXF bundle publisher is a separate concern that survives the deprecation. We avoid the coupling by importing `createUxfCarPublisher` directly rather than passing `tokenSync.ipfs.enabled: true` to `createNodeProviders`. New config field `SphereProvidersConfig.ipfsGateways` lets callers override the gateway list (defaults to `DEFAULT_IPFS_GATEWAYS` which honors the `SPHERE_IPFS_GATEWAY` env override). Pass an empty array to disable the publisher entirely (sends > RELAY_SAFE_CAP_BYTES will then fail at the SDK's `INLINE_CAR_TOO_LARGE` pre-flight). Verified end-to-end via the round-trip soak at sphere-sdk:manual-test-roundtrip-391.sh with STRICT_CID_DELIVERY=1: 4-hop A→B→A→B→A succeeded, balance reconciliation passed (alice -0.5 UCT, bob +0.5 UCT), no DUPLICATE_BUNDLE_MEMBERSHIP, no INLINE_CAR_TOO_LARGE. With sphere-sdk #394b's 512 KiB cap the realistic 121 KB bundle stays inline so CID delivery isn't actually exercised here; for >512 KiB bundles the publisher path is the same mechanism, end-to-end testing pending soak coverage for that range. Pairs with sphere-sdk PR (branch feat/issue-394-cid-delivery-wiring). Co-authored-by: Vladimir Rogojin <vrogojin@blockyinnovations.com>
The previous pin (02cb4550, sphere-sdk integration/all-fixes after
PR #225) predates sphere-sdk PR #394 ("automated CID delivery"), so
CI typecheck failed with:
src/legacy/legacy-cli.ts: Property 'deliverInvoice' does not
exist on type 'AccountingModule'.
src/shared/sphere-providers.ts: Module has no exported member
'createUxfCarPublisher' / 'DEFAULT_IPFS_GATEWAYS' /
'PublishToIpfsCallback'.
These four symbols are all present on the current sphere-sdk main
tip (3f3dadf, "merge: PR #395 #394 automated CID delivery re-enabled
+ 512 KiB inline cap + demo playbook"). Bumping the pin unblocks
typecheck.
Bumping to main also avoids the recurrence of "unable to read tree"
that hit the earlier 86468103a pin: integration tips get rebased
away when sub-PRs are squash-merged into main, but commits on main
itself stay reachable.
Verified: `npx tsc --noEmit` clean against sphere-sdk @ 3f3dadf.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
integration/all-fixeshas been the rolling dev line for sphere-clisince the original CLI extraction. It is 50 commits ahead of
mainand accumulates every PR merged during the post-extraction stabilisation
window.
mainhas fallen out of date — features that the soak suite(
manual-test-full-recovery.sh,manual-test-accounting-roundtrip.sh,manual-test-roundtrip-391.shin the sphere-sdk repo) depend on(notably
sphere invoice deliver) exist onintegration/all-fixesbut not on
main.Re-aligning
maintointegration/all-fixesso that future PRs branchfrom a
mainthat the soaks can validate against, and so thatPR #33 (canonical UX) can rebase onto a working baseline.
What lands
50 commits spanning ~29 files (+6033 / -89). Highlights:
Features
sphere invoice deliversubcommand (#226 / PR feat(invoice)(#226): addsphere invoice deliversubcommand #18) — the missingpiece that lets the recovery + accounting-roundtrip soaks run.
invoice createaccepts@nametag/ chain pubkey /alpha1...in
--target.Fixes
invoice statusno longer crashes on unknown ID (invoice status crashes with 'Cannot read properties of undefined' when invoice not found locally #21 / PR fix(cli)(#21): throw ExitSignal from process.exit wrapper so handlers stop #22).--detachkeeps child alive pastprocess.disconnect()(daemon:
sphere daemon start --detachexits immediately, leaving a stale PID file #19 / PR fix(daemon)(#19): keep --detach child alive past process.disconnect() #20).invoice status/invoice listuse'full'sync mode so cross-devicestate replicates (invoice-status uses ensureSync(sphere, 'nostr') — should be 'full' to trigger IPFS pull #24 / PR fix(cli)(#24): invoice-status and invoice-list use 'full' sync mode #26).
detectWalletKindignores empty{}placeholder (Migrate from deprecated IpfsStorageProvider to createNodeProfileProviders (Profile + aggregator pointer + IPFS CAR) #23 / PR fix(cli)(#23): detectWalletKind ignores empty{}wallet.json placeholder #27).wallet useconfirmation routed to stderr (#282 / PR fix(cli)(sphere-sdk#282): routewallet useconfirmation to STDERR #29).buildSphereProviderswirespublishToIpfs+cidFetchGateways(sphere-sdk #394 / PR feat(cli)(sphere-sdk#394): wire publishToIpfs + cidFetchGateways in buildSphereProviders #31).
Test infrastructure (#156 series — PRs #13–#14)
multiaddress, assets, daemon, faucet, group, invoice, l1, market,
migrate, nametag, send, swap (offline + Docker-escrow e2e).
test/integration/local-infra/Docker-compose harness with relayand escrow stubs.
CI
Risk
Rollup of 50 commits that have each landed via their own reviewed PRs
(merge commits visible in
git log). No new code introduced by thisrollup PR itself — it's strictly a fast-forward / merge of already-reviewed
changes.
The 573-line touch in
src/legacy/legacy-cli.tswill conflict with theparallel PR #33 (canonical UX). Plan: merge this rollup first, then
rebase #33 onto the new
mainand resolve.Test plan
integration/all-fixes(recent CI is green except forthe unrelated pinned-SDK-SHA infra issue, which is itself fixed in
this rollup).
manual-test-full-recovery.sh,manual-test-roundtrip-391.sh,manual-test-accounting-roundtrip.shend-to-end against real testnet.